﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using System.IO;

/*
 * XNA Windows Phone Easy Microphone Access
 * by Brian MacIntosh for ThunderFish Entertainment/BoneFish Studios
 * 
 * Version: 1.0
 * 
 * 11/23/2011: Version 1.0
 * - Created
 */

/*
 * This class provides easy access to the Windows Phone microphone
 * hardware.  Simply initialize the microphone with ThunderFish.
 * TMicrophone.Initialize() when the app is started (OnNavigateTo)
 * and deinitialize it with ThunderFish.TMicrophone.Deinitialize()
 * when you are done (OnNavigateFrom).
 * 
 * You can then record audio from the microphone using StartRecording()
 * and StopRecording().
 * 
 * You can record a specific length of audio from the microphone using
 * StartRecording(duration).  The TimedRecordingDone event will be invoked
 * when the recording is done.
 * 
 * You can monitor the intensity of the microphone readings with the
 * Intesity field. The microphone must be recording for the field to
 * be updated - you can use StartMonitoring and StopMonitoring to
 * "fake" recording for this purpose.
 * 
 * See method descriptions for more information.
 */

namespace ThunderFish
{
    /// <summary>
    /// This class provides methods for accessing the phone microphone.
    /// </summary>
    public static class TMicrophone
    {
        private static Microphone mic;

        private static MemoryStream currentData;

        private static TimeSpan targetRecordTime;
        private static bool isTimed;

        private static TimeSpan defaultBufferTime;

        /// <summary>
        /// Returns true if the microphone is currently recording.
        /// </summary>
        public static bool IsRecording { get { return mic.State == MicrophoneState.Started; } }

        /// <summary>
        /// Returns the friendly name of the current microphone.
        /// </summary>
        public static string MicrophoneName { get { return mic.Name; } }

        /// <summary>
        /// Returns the sample rate (bytes per second) of the microphone.
        /// </summary>
        public static int SampleRate { get { return mic.SampleRate; } }

        /// <summary>
        /// Returns true if the current microphone is a headset.
        /// </summary>
        public static bool IsHeadset { get { return mic.IsHeadset; } }


        public delegate void TimedRecordingEvent(SoundEffect record);

        /// <summary>
        /// Event that occurs when a timed recording has finished.
        /// </summary>
        public static event TimedRecordingEvent TimedRecordingDone;


        /// <summary>
        /// Set up the microphone.
        /// </summary>
        public static void Initialize()
        {
            mic = Microphone.Default;
            defaultBufferTime = mic.BufferDuration;
            mic.BufferReady += BufferSound;
        }

        /// <summary>
        /// Unload the microphone.
        /// </summary>
        public static void Deinitialize()
        {
            if (IsRecording)
                mic.Stop();
            mic.BufferReady -= BufferSound;
            if (currentData != null) currentData.Close();
        }

        /// <summary>
        /// Switches to recording from a headset microphone if one is
        /// available.
        /// </summary>
        /// <returns>True if a headset mic was found</returns>
        public static bool UseHeadset()
        {
            if (IsRecording) throw new MicrophoneAlreadyRecordingException();
            foreach (Microphone m in Microphone.All)
            {
                if (m.IsHeadset)
                {
                    mic = m;
                    defaultBufferTime = mic.BufferDuration;
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Switches to recording from the built-in microphone.
        /// </summary>
        /// <returns>True if the built-in mic was found</returns>
        public static bool UseBuiltIn()
        {
            if (IsRecording) throw new MicrophoneAlreadyRecordingException();
            foreach (Microphone m in Microphone.All)
            {
                if (!m.IsHeadset)
                {
                    mic = m;
                    defaultBufferTime = mic.BufferDuration;
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Get the last sound intensity reading from the mic.
        /// Background noise is about 0.25, significant noises are 0.3 up to 0.5
        /// </summary>
        public static float Intensity { get; private set; }

        /// <summary>
        /// Start the mic to monitor for sound
        /// </summary>
        public static void StartMonitoring()
        {
            mic.BufferDuration = TimeSpan.FromMilliseconds(100);
            mic.Start();
        }

        /// <summary>
        /// Stop monitoring for sound
        /// </summary>
        public static void StopMonitoring()
        {
            mic.Stop();
            mic.BufferDuration = defaultBufferTime;
        }

        /// <summary>
        /// Begin asynchronously recording data from the microphone.
        /// </summary>
        public static void StartRecording()
        {
            mic.BufferDuration = defaultBufferTime;
            currentData = new MemoryStream();
            mic.Start();
            isTimed = false;
        }

        /// <summary>
        /// Begin asynchronously recording data from the microphone.
        /// Recording will stop automatically after the specified duration
        /// and the TimedRecordingDone event will occur.
        /// </summary>
        /// <param name="duration"></param>
        public static void StartRecording(TimeSpan duration)
        {
            mic.BufferDuration = defaultBufferTime;
            currentData = new MemoryStream();
            mic.Start();
            targetRecordTime = duration;
            isTimed = true;
        }

        /// <summary>
        /// Stops recording and returns a SoundEffect of the recorded data.
        /// </summary>
        /// <returns>A SoundEffect of the recorded data</returns>
        public static SoundEffect StopRecording()
        {
            if (!IsRecording) throw new MicrophoneNotStartedException();
            mic.Stop();
            SoundEffect ret =
                new SoundEffect(currentData.ToArray(), mic.SampleRate, AudioChannels.Mono);
            currentData.Close();
            currentData = null;
            return ret;
        }

        private static void BufferSound(object sender, EventArgs e)
        {
            byte[] buffer = new byte[mic.GetSampleSizeInBytes(mic.BufferDuration)];
            int bytesRead = 0;

            while ((bytesRead = mic.GetData(buffer, 0, buffer.Length)) > 0)
            {
                //Record intensity
                int total = 0;
                for (int c = 0; c < buffer.Length; c++)
                    total += buffer[c] * buffer[c];
                Intensity = 1 - (total / (float)buffer.Length) / (256 * 256);

                if (currentData != null)
                {
                    if (isTimed
                        && currentData.Length + bytesRead >= mic.GetSampleSizeInBytes(targetRecordTime))
                    {
                        currentData.Write(buffer, 0,
                            mic.GetSampleSizeInBytes(targetRecordTime) - (int)currentData.Length);
                        TimedRecordingDone(StopRecording());
                    }
                    else
                        currentData.Write(buffer, 0, bytesRead);
                }
            }
        }

        private class MicrophoneAlreadyRecordingException : Exception
        {
            public override string Message
            {
                get
                {
                    return "Attempted to change microphones while recording.";
                }
            }
        }

        private class MicrophoneNotStartedException : Exception
        {
            public override string Message
            {
                get
                {
                    return "Attempted to get recording data when microphone was not recording.";
                }
            }
        }
    }
}
